/*
  Copyright 1996-2003
  Simon Whiteside

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

  $Id: XMLElementObject.java,v 1.2 2003/05/06 13:38:04 simkin_cvs Exp $
*/
package simkin;
import java.util.*;
import org.kxml.kdom.*;
import org.kxml.*;
import org.kxml.io.*;
import org.kxml.parser.*;

/**
 * This object is a container for an XML element, and exposes an interface to it to Simkin
 * This class is used with the KXML (http://kxml.enhydra.org) parser and DOM classes
 * The class implements methods from the Executable interface.
 *<p>The method getValue, setValue and method all search for matching element tags within the XML document. Only the first matching tag is used.  
 * <p>This class supports the following fields: <ul>
 * <li>numChildren - the number of children of this element</li>
 * </ul>
 */
public class XMLElementObject extends ExecutableRoot {
    /** A string indicating the location */
    String m_Location="";
    /**
     * the underlying XML Element itself
     */
    Element m_Element=null;
    /**
     * This caches the parse trees for the methods already come across
     */
    Hashtable m_MethodCache=null;
    /**
     * this variable controls whether new items are added to this element if they are not found, by default it is false,
     but can be modified using the setAddIfNotPresent() method
    */
    boolean m_AddIfNotPresent=false;
    /**
     * Default Constructor 
     */
    public XMLElementObject(){
    }
    /**
     * Constructor which takes an Element
     * @param location location of this element
     * @param elem the element to be stored
     */
    public XMLElementObject(String location,Element elem){
	m_Location=location;
	setElement(elem);
    }
    /**
     * This method updates the associated element and clears the parse tree cache
     * @param elem - the new Element
     */
    protected void setElement(Element elem){
	m_Element=elem;
	m_MethodCache=null;
    }
    /**
     * @return the value of the element text data as a string
     */
    public String toString(){
	String value="";
	if (m_Element!=null)
	    value=getData(m_Element);
	return value;
    }
    /**
     * sets the value of an item in the element. If the value passed is an element, it is first copied. If the m_AddIfNotPresent flag is true, a new item will be added if one is not already present
     * @param name the name of the element tag to set (null if it's the overall element)
     * @param attrib the name of the attribute to set (null to set text for the element)
     * @throws RuntimeException - if there was a problem running the script (such as not having permission to access a field)
     * @throws NoSuchFieldException - if the field could not be found
     */
    public void setValue(String name,String attrib,Object v,ExecutableContext ctxt) throws RuntimeException, FieldNotSupportedException{
	boolean bFound=true;
	if (m_Element!=null){
	    boolean otherIsXML=v instanceof XMLElementObject;
	    Element child=m_Element;
	    if (name!=null)
		child=findChild(m_Element,name);
	    if (child==null && m_AddIfNotPresent==false){
		bFound=false;
	    }else{
		if (child==null && m_AddIfNotPresent==true){
		    child=new NamedElement(name);
		    m_Element.addChild(Xml.ELEMENT,child);
		}
		if (otherIsXML){
		    ((XMLElementObject)v).copyItemsInto(child);
		    ((XMLElementObject)v).copyAttributesInto(child);
		}else
		    if (attrib==null){
			setData(child,v.toString());
		    }else
			child.setValue(attrib,v.toString());
	    }
	}else
	    bFound=false;
	if (bFound==false)
	    super.setValue(name,attrib,v,ctxt);
    }
    /**
     * sets the value of an item in the element. If the value passed is an element, it is first copied. If the m_AddIfNotPresent flag is true, a new item with the tag name "array_item" will be added if one is not already present
     * @param array_index the position of the element in the list
     * @param attrib the name of the attribute to set (null to set text for the element)
     * @throws RuntimeException - if there was a problem running the script
     */
    public void setValueAt(Object array_index,String attrib,Object v,ExecutableContext ctxt) throws RuntimeException{
	boolean bFound=true;
	if (m_Element!=null){
	    boolean otherIsXML=v instanceof XMLElementObject;
	    int array_index_value=Interpreter.intValue(array_index);
	    Element child=findChild(m_Element,array_index_value);
	    if (child==null && m_AddIfNotPresent==false){
		bFound=false;
	    }else{
		if (child==null && m_AddIfNotPresent==true){
		    int num_children=m_Element.getChildCount();
		    if (array_index_value>=num_children){
			int num_to_add=array_index_value-num_children+1;
			for (int i=0;i<num_to_add;i++){
			    child=new NamedElement("array_item");
			    m_Element.addChild(Xml.ELEMENT,child);
			}
		    }
		}
		if (otherIsXML){
		    ((XMLElementObject)v).copyItemsInto(child);
		    ((XMLElementObject)v).copyAttributesInto(child);
		}else
		    if (attrib==null)
			setData(child,v.toString());
		    else
			child.setValue(attrib,v.toString());
	    }
	}else
	    bFound=false;
	if (bFound==false)
	    super.setValueAt(array_index,attrib,v,ctxt);
    }
    /**
     * This method clears the other elements attributes and copies from ours into it
     * @param child - the element into which our attributes will be copied
     */
    public void copyAttributesInto(Element other){
        m_Element.setAttributes(other.getAttributes());
        other.setAttributes(new Vector());
    }

    /**
     * Clears the other element and does a deep copy of the children of this node into that one
     * @param child - the element into which our children will be copied
     */
    public void copyItemsInto(Element other){
        if (other!=m_Element){
            int num_children=other.getChildCount();
            for (int i=0;i<num_children;i++){
                int type=other.getType(0);
                Object node=other.getChild(0);
                other.removeChild(0);
                m_Element.addChild(type,node);
            }
        }
    }
    /**
     * tests for equality with another object, using the string value
     * @return true if the data in both elements is the same
     */
    public boolean equals(Object o){
	return toString().equals(o.toString());
    }
    /**
     * This method returns the XML Element being held by the object.
     * @return the underlying Element
     */
    public Element getElement(){
	return m_Element;
    }
    /**
     * Retrieves the nth child element from this element. It returns null if the object could not be found.If the m_AddIfNotPresent flag is true, a new item with the tag name "array_item" will be added if one is not already present
     * @param array_index the array index
     * @param attrib the attribute name to retrieve
     * @return the value of the field
     * @throws RuntimeException - if there was a problem running the script 
     */
    public Object getValueAt(Object array_index,String attrib,ExecutableContext ctxt) throws RuntimeException{
	Object ret=null;
	if (m_Element!=null){
	    int array_index_value=Interpreter.intValue(array_index);
	    Element child=findChild(m_Element,array_index_value);
	    if (child==null && m_AddIfNotPresent){
		int num_children=m_Element.getChildCount();
		if (array_index_value>=num_children){
		    int num_to_add=array_index_value-num_children+1;
		    for (int i=0;i<num_to_add;i++){
			child=new NamedElement("array_item");
			m_Element.addChild(Xml.ELEMENT,child);
		    }
		}
	    }
	    if (child!=null){
		if (attrib!=null)
		    ret=child.getAttribute(attrib);
		else
		    ret=createXMLElementObject(Utils.addStrings(m_Location,"[",array_index.toString(),"]"),child);
	    }else
		ret=super.getValueAt(array_index,attrib,ctxt);
	}else
	    ret=super.getValueAt(array_index,attrib,ctxt);
	return ret;
    }
    /**
     * Retrieves a field from the XML. The value returned is an XMLElementObject, unless the attrib value is specified.If the m_AddIfNotPresent flag is true, a new item will be added if one is not already present
     * @param name the tag name containing the data
     * @param attrib the attribute name to retrieve
     * @return the value of the field
     * @throws RuntimeException - if there was a problem running the script (such as not having permission to access a field)
     * @throws FieldNotSupportedException - if the field could not be found
     */
    public Object getValue(String name,String attrib,ExecutableContext ctxt) throws RuntimeException, FieldNotSupportedException{
	Object ret=null;
	if (m_Element!=null){
	    if (name!=null && name.equals("nodename")){
		ret=tagName();
	    }else
                if (name!=null && name.equals("numChildren")){
                    int count=0;
                    for (int i=0;i<m_Element.getChildCount();i++){
                        if (m_Element.getType(i)==Xml.ELEMENT){
                            count++;
                        }
                    }
                    return new Integer(count);
                }else{
                    Element child=m_Element;
                    if (m_Element!=null){
                        if (name!=null){
                            child=findChild(m_Element,name);
                        }
                    }
                    if (child==null && m_AddIfNotPresent){
                        child=new NamedElement(name);
                        m_Element.addChild(Xml.ELEMENT,child);
                    }
                    if (child!=null){
                        if (attrib==null)
                            ret=createXMLElementObject(Utils.addStrings(m_Location,":",name),child);
                        else
                            ret=child.getAttribute(attrib);
                    }else{
                        ret=super.getValue(name,attrib,ctxt);
                    }
                }
	}else
	    ret=super.getValue(name,attrib,ctxt);
	return ret;
    }
    /**
     * retrieves the text data from an element
     * @param element
     * @return the text from a child node of type CDATA or TEXT
     */
    static public String getData(Element element){
	StringBuffer str=new StringBuffer();
	for (int i=0;i<element.getChildCount();i++){
	    int type=element.getType(i);
	    if (type==Xml.TEXT){
		str.append((String)(element.getChild(i)));
	    }
	}
	return str.toString();
    }
    /**
     * sets the text data for a node by looking for the first CDATA and TEXT child node
     * @param element the element to be changed
     * @param data the data to be set
     */
    public static void setData(Element element,String data){
	boolean found=false;
	for (int i=0;i<element.getChildCount();i++){
	    int type=element.getType(i);
	    if (type==Xml.TEXT){
                element.addChild(i,Xml.TEXT,data);
		found=true;
		break;
	    }
	}
	if (found==false){
            element.addChild(Xml.TEXT,data);
	}
    }
    /**
     * returns a child element
     * @param parent the parent element
     * @param tagname the tag name of the element
     * @return the first element child of the parent with the matching tag name, or null if not found
     */
    static public Element findChild(Element parent,String tagname){
	Element ret=null;
	for (int i=0;i<parent.getChildCount();i++){
            if (parent.getType(i)==Xml.ELEMENT){
                Element node=(Element)parent.getChild(i);
                if (node.getName().equals(tagname)){
                    ret=node;
                    break;
                }
	    }
	}
	return ret;
    }
    /**
     * returns the nth child element
     * @param parent the parent element
     * @param tagname the tag name of the element
     * @return the nth  child of the parent, or null if not found
     */
    static public Element findChild(Element parent,int index){
	Element ret=null;
	if (index<parent.getChildCount()){
	    int element_count=0;
	    for (int i=0;i<parent.getChildCount();i++){
                if (parent.getType(i)==Xml.ELEMENT){
                    Element node=(Element)parent.getChild(i);
		    if (element_count==index){
			ret=(Element)node;
			break;
		    }else
			element_count++;
		}
	    }
	}
	return ret;
    }
    /**
     * returns a child element with the given attribute set to the given value
     * @param parent the parent element
     * @param tagname the tag name of the element
     * @param attribute name of the attribute
     * @param value value of the named attribute
     * @return the first element child of the parent with the matching tag name and attribute value, or null if not found
     */
    static public Element findChild(Element parent,String tagname,String attribute,String value){
	Element ret=null;
	for (int i=0;i<parent.getChildCount();i++){
            if (parent.getType(i)==Xml.ELEMENT){
                Element node=(Element)parent.getChild(i);
                if (node.getName().equals(tagname)){
                    if (node.getValue(attribute).equals(value)){
                        ret=(Element)node;
                        break;
                    }
                }
	    }
	}
	return ret;
    }
    /**
     * Sets an attribute on this node
     */
    public void setAttribute(String name,String value){
	if (m_Element!=null)
	    m_Element.setValue(name,value);
    }
    /**
     * This method returns the value of an attribute attached to this element.
     * @return the value of the given attribute
     */
    public String getAttribute(String name){
	String value="";
	if (m_Element!=null)
	    value=m_Element.getValue(name);
	return value;
    }
    /**
     * This method creates a new XML Element object to wrap an element. Override this for special behaviour in derived classes. In this method, the newly created object inherits this object's m_AddIfNotPresent flag
     */
    protected XMLElementObject createXMLElementObject(String location,Element elem){
	XMLElementObject object=new XMLElementObject(location,elem);
	object.setAddIfNotPresent(getAddIfNotPresent());
	return object;
    }
    /**
     * This method returns an object which can iterate over all the immediate child elements of this element
     */
    public XMLElementObjectEnumerator enumerate(){
	return new XMLElementObjectEnumerator(this);
    }
    /**
     * This method returns an object which can iterate over all the immediate child elements of this element whose tagname matches the one given
     */
    public XMLElementObjectEnumerator enumerate(String tagName){
	return new XMLElementObjectEnumerator(this,tagName);
    }
    /**
     * This method writes the entire XML element out to the tracer
     */
    public void dump(){
	Tracer.trace(m_Element.toString());
    }
    /**
     * this method attempts to execute a method stored in the XML. It searches for an element whose tag matches the method name and if found passes the text for the tag through to the interpeter
     * @param s the name of the method
     * @param args an array of arguments to the method
     * @param ret the object to receive the result of the method call
     * @param interp interpreter
     * @return true if the method was found, false otherwise
     * @throws ParseException if the code contained syntax errors
     * @throws RuntimeException if there was an error running the code
     * @throws MethodNotSupportedException if the method could not be found
     */
    public Object method(String s,Object[] args,ExecutableContext ctxt)  throws ParseException, RuntimeException,MethodNotSupportedException{
	Object ret=null;
	if (m_Element!=null){
	    ParseNode parseNode=null;
	    if (m_MethodCache!=null)
		parseNode=(ParseNode)m_MethodCache.get(s);
	    if (parseNode==null){
		Element node=XMLElementObject.findChild(m_Element,"function","name",s);
		if (node!=null){
		    String location=Utils.addStrings(m_Location,":",s);
		    String code=XMLElementObject.getData(node);
		    Vector paramList=new Vector();
		    String params=node.getValue("params");
                    if (params!=null){
                        StringTokenizer tokenizer=new StringTokenizer(params,", ");
                        while (tokenizer.hasMoreTokens())
                            paramList.addElement(tokenizer.nextToken());
                    }
		    code=Utils.removeInitialBlankLines(code);
		    ExecuteResult result=ctxt.getInterpreter().executeStringExternalParams(location,this,paramList,code,args,ctxt);
		    ret=result.getResult();
		    parseNode=result.getParseTree();
		    if (m_MethodCache==null)
			m_MethodCache=new Hashtable();
		    m_MethodCache.put(s,parseNode);
		}else
		    ret=super.method(s,args,ctxt);
	    }else{	
		String location=Utils.addStrings(m_Location,":",s);
		ret=ctxt.getInterpreter().executeParseTree(location,this,parseNode,args,ctxt);
	    }
	}else
	    ret=super.method(s,args,ctxt);
	return ret;
    }
    /** this method returns the tag name of this object (taken from the code element)*/
    public String tagName(){
	String name="";
	if (m_Element!=null)
	    name=m_Element.getName();
	return name;
    }
    class NamedElement extends Element {
	NamedElement(String tag){
            namespace=Xml.NO_NAMESPACE;
            name=tag;
	}
    }
    /** adds a new element with the given tag name to this element 
     * @return the newly created element
     */
    public XMLElementObject addElement(String tag_name){
	XMLElementObject o_elem=null;
	if (m_Element!=null){
	    Element elem=new NamedElement(tag_name);
	    m_Element.addChild(Xml.ELEMENT,elem);
	    o_elem=createXMLElementObject(Utils.addStrings(m_Location,":",tag_name),elem);
	}
	return o_elem;
    }
    /** returns true if this element contains an element with the given tag name */
    public boolean containsElement(String tag_name){
	boolean contains=false;
	if (m_Element!=null)
	    contains=(findChild(m_Element,tag_name)!=null);
	return contains;
    }
    /** sets the flag controlling whether new elements are created as they are accessed
     * @param enable enables this feature (which by default is disabled)
     */
    public void setAddIfNotPresent(boolean enable){
	m_AddIfNotPresent=enable;
    }
    /** this returns the value of the flag controlling whether new elements are created as they are accessed 
     * @return true if the feature is enabled, otherwise false (the default)
     */
    public boolean getAddIfNotPresent(){
	return m_AddIfNotPresent;
    }
    /** This method returns an XMLElementObjectEnumerator object which is used in a foreach statement. This version iterates unconditionally over all the children of this element.
     * @return a new iterator object, or null if this object cannot be iterated
     */
    public ExecutableIterator createIterator(){
	return new XMLElementObjectEnumerator(this);
    }
    /** This method returns an XMLElementObjectEnumerator object which is used in a foreach statement. This version is iterates over the child elements with the matching tag name.
     * @param qualifier - a string, children with a matching tag name will be returned
     * @return a new iterator object, or null if this object cannot be iterated
     */
    public ExecutableIterator createIterator(String qualifier){
	return new XMLElementObjectEnumerator(this,qualifier);
    }
    /**
     * This method returns the source for a scripted method described by the location. 
     */
    public String getSource(String location){
	String src="";
	if (m_Element!=null){
	    String method_name=location;
	    int last_colon=location.lastIndexOf(':');
	    if (last_colon!=-1)
		method_name=location.substring(last_colon+1);
	    Element node=XMLElementObject.findChild(m_Element,"function","name",method_name);
	    if (node!=null)
		src=XMLElementObject.getData(node);
	    src=Utils.removeInitialBlankLines(src);
	}
	return src;
    }
    /**
     * This method returns the instance variables for this object. This is a hashtable mapping name to XMLElementObject.
     * If names are duplicated the last occurrence will appear in the table
     */
    public Hashtable getInstanceVariables(){
	Hashtable hash=new Hashtable();
	if (m_Element!=null){
	    for (int i=0;i<m_Element.getChildCount();i++){
                if (m_Element.getType(i)==Xml.ELEMENT){
                    Element node=(Element)m_Element.getChild(i);
		    String name=node.getName();
		    hash.put(name,new XMLElementObject(name,node));
		}
	    }
	}
	return hash;
    }
    /**
     * This method returns the attributes for this object. At 
     * @return table a table filled with the attribute values. Each entry in the table maps an attribute name to a value
     */
    public Hashtable getAttributes(){
	Hashtable hash=new Hashtable();
	if (m_Element!=null){
            Vector attributes=m_Element.getAttributes();
	    for (int i=0;i<attributes.size();i++){
                Attribute attr=(Attribute)attributes.elementAt(i);
		hash.put(attr.getName(),attr.getValue());
	    }
	}
	return hash;
    }
    /** returns the location of this element */
    public String getLocation(){
	return m_Location;
    }
}
